#!/usr/bin/python

import socket
import time
import xml.etree.cElementTree as et
from threading import Thread, Lock
from sets import Set
import Queue
import sys
import struct
import logging

def synchronize():
    sync_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    
    #generate time offset
    # |------------ts-------------- timeserver
    #          /      \
    #       send      reply
    #       /            \
    # |----t0-----ts'-----t1-------- node
    # 
    # ts' ~ ( t1 + t0 ) / 2 
    # ts = ts' + delta_t -> delta_t = ts' - ts
    # delta_t ~ ( t1 + t0 ) / 2 - ts
    
    t0 = time.time()
    sync_sock.sendto('time', (TS_ADDR, TS_PORT))
    
    time_sync = sync_sock.recv(64)
    
    t1 = time.time()
    
    t_sync = struct.unpack('d', time_sync)[0]
    
    delta = (t1 + t0) / 2 - t_sync
    
    sync_sock.close()
    
    return delta
    
def process_packet(bytes, addr):
    
    #structure of packet
    #|------------------------------------------------
    #| Function     | Control Bit | Data        
    #|----------------------------------------------
    #| Update Table |     0       | <xml data> 
    #|-----------------------------------------------
    #| Broadcast    |     1       | <PacketNumber, BroadcastNodeID, SourceNodeID, DestinationNodeID, Data>
    #|----------------------------------------------------
    
    control_bit = bytes[0]
    logging.info( 'Message recieved with control bit: ' + control_bit )
    #branch on control bit
    if control_bit == '0':
        #do stuff for updatring table
        temp_bnode = update_table(bytes[1:])
        logging.debug( table )
        #add time_data entry for the broadcast node
        if temp_bnode not in time_data:
            time_data[temp_bnode] = []
            
    elif control_bit == '1':
        #broadcast the data
        # need to determine if this node is the destination
        # in that case, need to get timing info
        broadcast_node_id = socket.inet_ntoa(bytes[1:5])
        
        destination_node_id = socket.inet_ntoa(bytes[5:9])
        
        msg = struct.unpack('d',bytes[9:])[0]
        
        if destination_node_id == this_node_id:
            
            t = time.time() #float
            time_data[broadcast_node_id].append((msg, t))
            logging.info( 'Message received... @%f: %f' % (t, msg))
        else:
                
            #Add packet to queue
            new_p = (broadcast_node_id, destination_node_id, bytes)
            logging.debug( new_p )
            packet_queue.put(new_p)
            
    elif control_bit == '2':#test
        #send debugging info
        #  right now just number of threads
        t_count = 0
        for t in thread_bag:
            if t.isAlive:
                t_count += 1
        
        thread_debug_msg = 'Number of threads active: ' + str(t_count)
        temp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        temp_sock.sendto(thread_debug_msg, addr)#changed this to reply with msg sent
        temp_sock.close()
        msg = bytes[1:]
        logging.debug( thread_debug_msg )
        
    elif control_bit == '3':#terminate transmission
        #incoming packet structure
        # : '3' + packed ip broadcast_node_id
        central_node_ip = '128.42.142.45'# could change
        central_node_port = 50006
        broadcast_node_id = socket.inet_ntoa(bytes[1:5])
        #delete routingtable
        goat = 3
        #process data
        #time_data[broadcast_node] = [(broad_time_float, recv_time_float), ...]
        temp_float_list = [item for sublist in time_data[broadcast_node_id] for item in sublist]
        logging.info( "Time Data List: " )
        logging.info( temp_float_list )
        time_data_string = struct.pack(str(len(temp_float_list)) + 'd', *temp_float_list)
        packed_time_offset = struct.pack("d", delta_t)
        #send data
        #[control_bit][broadcast node][dest node][(source_time_float, dest_time_float),..]
        # control bit = 1 
        new_msg = '1' + bytes[1:5] + bytes[5:9] + packed_time_offset + time_data_string
        
        temp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        temp_sock.sendto(new_msg, (central_node_ip, central_node_port))
        
        #delete time data
        time_data[broadcast_node_id] = []
        
        temp_sock.close()
    
    elif control_bit == '4':
        logging.info('Exit signal received...')
        temp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        temp_sock.sendto('Ack', addr)
        temp_sock.close()
        sys.exit()
        
    elif control_bit == '5':
        #ping each node 10 times
        #calculate average RTT
        #estimate Oneway latency
        temp_node_list = [socket.inet_ntoa(bytes[1:][i:i+4]) for i in range(0, len(bytes[1:]), 4)]
        
        #[(node_ip, oneway latecy), ...]
        temp_latency_list = []
        
        temp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        NUM_PACKETS = 10
        for temp_node in temp_node_list:
            logging.debug("Sending packet to: %s", temp_node)
            temp_addr = (temp_node, PORT)
            #send ten packets
            temp_time = []
            temp_sock.settimeout(2)
            for i in range(NUM_PACKETS):
                t0 = time.time()
            
                temp_sock.sendto('2', temp_addr)
                try:
                    temp_sock.recvfrom(64)
                except socket.error, msg:
                    print msg
                    continue
                
                t1 = time.time()
            
                temp_time.append((t1 - t0) * 500) #/ 2 * 1000 ten packets, one-way, in millisec
            
            if len(temp_time) > 0:
                temp_latency_list.append((temp_node, sum(temp_time)/float(len(temp_time))))
        
        #broadcast latency list
        ret_val = ''
        for item in temp_latency_list:
            temp_ip = socket.inet_aton(item[0])
            temp_time = struct.pack('d', item[1])
            ret_val += temp_ip + temp_time
        
        temp_sock.sendto(ret_val, addr)
        temp_sock.close()
    
    else:
        return False
    
    return True
        
def update_table(bytes):
    
    root = et.fromstring(bytes)
    
    for child in root:
        #print child.tag + child.attrib
        table_lock.acquire(True)
        table[child.attrib['broadcast_node']] = dict()
        for child2 in child:
            table[child.attrib['broadcast_node']][child2.text] = child2.attrib['next_hop']
        table_lock.release()
    logging.debug( table )
    return root[0].attrib['broadcast_node']#maybe????
            
def broadcast():
    
    broad_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  
    
    #Enter a loop to check shared queue and send any outstnding packets
    while 1:
        
        bytes = packet_queue.get(block=True)
        
        broadcast_node_id = bytes[0]
        
        destination_node_id = bytes[1]
        
        dest_ip = table[broadcast_node_id][destination_node_id] #correct for setup
            
        dest_port = PORT
            
        new_msg = bytes[2]
            
        broad_sock.sendto(new_msg, (dest_ip, dest_port))
        
    broad_sock.close()

#process args
loglevel = 'WARNING'
for st in sys.argv[1:]:
    if st.startswith('--log='):
        loglevel = st.split('=')[1]
    
#initialize logging
numeric_level = getattr(logging, loglevel.upper(), None)
if not isinstance(numeric_level, int):
    raise ValueError('Invalid log level: %s' % loglevel)
logging.basicConfig(level=numeric_level, format='%(levelname)s:%(message)s')

#Synchronyse with time server
# should this be reactive? like signal driven
TS_ADDR = '192.12.33.102'
TS_PORT = 50008
delta_t = synchronize()
logging.info('Time Syncronized with delta of: ' + str(delta_t) + ' milliseconds')

#Stores a Mapping from Source/Destination to Next Hop
# Need to store ip for each node instead of id
#|------------------------------------------------|
#| Source   |   Destination   |    Next Hop       |
#|------------------------------------------------|
#|   1      |       5         |       3           |
#|------------------------------------------------|

#    
table = dict()
table_lock = Lock()

#dict to store test data
time_data = dict()

#shared queue for broadcasting packets
# each queue entry should be a tuple   
# containing the next hop ip and data
# (next_hop_ip, data)
packet_queue = Queue.Queue(maxsize=0)

#Threads to 
NUM_BROADCAST_THREADS = 100#increased this to see more effective transmit
thread_bag = Set()# Add this to enable querying
for i in range(NUM_BROADCAST_THREADS):
     t = Thread(target=broadcast)
     t.setDaemon(True)
     t.start()
     thread_bag.add(t)
     

HOST = ''                 # Symbolic name meaning all available interfaces
PORT = 50007              # Arbitrary non-privileged port

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

this_node_id = socket.gethostbyname(socket.gethostname())
logging.info('Node ip: ' + this_node_id)
logging.info('Socket Created. Initializing...')
try:
    s.bind((HOST, PORT))
except socket.error, msg:
    logging.error('Bind failed. Error code: ' +str(msg[0]) + ', Error message : ' + msg[1])
    sys.exit();

print 'Socket bound on port: ' + str(PORT)

#hang out and listen for stuffs
while 1:
    data, addr = s.recvfrom(4096)
    logging.info( 'Connected with' + addr[0] + str(addr[1]))
    #process data
    #temp_tread = Thread(target=process_packet, args=(data, addr))
    #temp_tread.start()
    process_packet(data, addr)
    






